package com.lemon.exam.service.impl;

import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.ResponseBody;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.util.ObjectBuilder;
import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.util.SecurityUtil;
import com.lemon.exam.common.web.Param;
import com.lemon.exam.entity.po.LoggingPO;
import com.lemon.exam.repository.LoggingRepository;
import com.lemon.exam.service.ILoggingService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchClient;
import org.springframework.http.ContentDisposition;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;

import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;

/**
 * 日志 服务实现类
 *
 * @author Lemon
 * @since 2025/4/25 11:58
 */
@Slf4j
@Service
public class LoggingServiceImpl implements ILoggingService {
    // 样式缓存
    private static final Map<String, CellStyle> STYLE_CACHE = new ConcurrentHashMap<>();

    // Excel表头
    private static final String[] EXCEL_HEADERS = {"IP地址", "请求地址", "请求说明", "请求方式", "参数", "请求体参数", "耗时(ms)", "状态码", "创建时间"};

    // 重试配置
    private static final RetryBackoffSpec RETRY_SPEC = Retry
            .backoff(3, Duration.ofMillis(100))
            .maxBackoff(Duration.ofSeconds(1))
            .jitter(0.5)
            .doBeforeRetry(s -> log.warn("正在重试日志插入（尝试{}）", s.totalRetries()));

    @Resource
    private LoggingRepository repository;
    @Resource
    private ReactiveElasticsearchClient client;

    /**
     * 添加日志
     * 第一次添加失败时，会异步重试
     *
     * @param loggingPO
     * @return
     */
    public Mono<Void> insert(LoggingPO loggingPO) {
        return repository.save(loggingPO)
                .retryWhen(RETRY_SPEC)
                .onErrorResume(e -> {
                    log.error("日志插入失败：{}", e.getMessage());
                    return Mono.empty();
                })
                .subscribeOn(Schedulers.boundedElastic())
                .then();
    }

    @Override
    public Mono<LoggingPO> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public Mono<Page<LoggingPO>> page(Param param) {
        return buildRequest(param).flatMap(request -> client.search(request, LoggingPO.class)
                .map(response -> {
                    List<LoggingPO> content = response.hits().hits().stream().map(Hit::source).toList();
                    TotalHits totalHits = response.hits().total();
                    Pageable pageRequest = PageRequest.of(param.getPageNum(), param.getPageSize());
                    return new PageImpl<>(new ArrayList<>(content), pageRequest, totalHits != null ? totalHits.value() : 0L);
                }));
    }

    /**
     * 构建查询请求
     *
     * @param param
     * @return
     */
    private Mono<SearchRequest> buildRequest(Param param) {
        return SecurityUtil.getUserId()
                .map(userId -> SearchRequest.of(s -> s
                        .index(ApiConst.ES_LOGGING_INDEX)
                        .query(buildQuery(param, userId))
                        .from(param.getPageNum() * param.getPageSize())
                        .size(param.getPageSize())
                        .sort(buildSort(param))));//构建排序
    }

    /**
     * 构建排序
     *
     * @param param
     * @return
     */
    @NotNull
    private static Function<SortOptions.Builder, ObjectBuilder<SortOptions>> buildSort(Param param) {
        return builder -> {
            //添加排序字段-正序
            if (null != param.getAsc() && param.getAsc().length > 0) {
                Arrays.stream(param.getAsc()).forEach(field -> builder.field(f -> f.field(field).order(SortOrder.Asc)));
            }
            //添加排序字段-倒序
            if (null != param.getDesc() && param.getDesc().length > 0) {
                Arrays.stream(param.getDesc()).forEach(field -> builder.field(f -> f.field(field).order(SortOrder.Desc)));
            }
            return builder;
        };
    }

    /**
     * 构建查询条件
     *
     * @param param
     * @return
     */
    @NotNull
    @SuppressWarnings("ConstantConditions")
    private static Function<Query.Builder, ObjectBuilder<Query>> buildQuery(Param param, Long userId) {
        return q -> q.bool(b -> {
            //用户ID
            b.must(m -> m.match(t -> t.field("userId").query(userId)));
            //其他条件
            Map<String, Object> map = param.getParam();
            if (CollectionUtils.isEmpty(map)) {
                return b;
            }
            Optional.ofNullable(map.get("code")).filter(code -> null != code && !"".equals(code)).ifPresent(code -> b.must(m -> m.match(t -> t.field("code").query(Integer.parseInt(String.valueOf(code))))));
            Optional.ofNullable(map.get("uri")).filter(uri -> null != uri && !"".equals(uri)).ifPresent(uri -> b.must(m -> m.match(t -> t.field("uri").query(String.valueOf(uri)))));
            Optional.ofNullable(map.get("method")).filter(method -> null != method && !"".equals(method)).ifPresent(method -> b.must(m -> m.match(t -> t.field("method").query(String.valueOf(method)))));
            Optional.ofNullable(map.get("minDuration")).filter(min -> null != min && !"".equals(min)).ifPresent(min -> b.filter(f -> f.range(r -> r.number(n -> n.field("duration").gte(Double.valueOf(String.valueOf(min)))))));
            Optional.ofNullable(map.get("maxDuration")).filter(max -> null != max && !"".equals(max)).ifPresent(max -> b.filter(f -> f.range(r -> r.number(n -> n.field("duration").lte(Double.valueOf(String.valueOf(max)))))));
            Optional.ofNullable(map.get("minCreateTime")).filter(min -> null != min && !"".equals(min)).ifPresent(min -> b.filter(f -> f.range(r -> r.number(n -> n.field("createTime").gte(Double.valueOf(String.valueOf(min)))))));
            Optional.ofNullable(map.get("maxCreateTime")).filter(max -> null != max && !"".equals(max)).ifPresent(max -> b.filter(f -> f.range(r -> r.number(n -> n.field("createTime").lte(Double.valueOf(String.valueOf(max)))))));
            return b;
        });
    }

    @Override
    public Mono<Void> export(Param param, ServerHttpResponse response) {
        //统一设置响应头
        response.getHeaders().setContentType(ApiConst.EXCEL_MEDIA_TYPE);
        //文件名
        String fileName = URLEncoder.encode("%s_%s.xlsx".formatted(ApiConst.ES_LOGGING_INDEX, LocalDateTime.now().format(ApiConst.DATE_FORMATTER_12)), StandardCharsets.UTF_8);
        response.getHeaders().setContentDisposition(ContentDisposition.attachment().filename(fileName).build());

        return buildRequest(param)
                .flatMap(request -> client.search(request, LoggingPO.class))
                .flatMap(this::convertResponseBodyToExcel)
                .flatMap(bytes -> response.writeWith(Flux.just(response.bufferFactory().wrap(bytes))))
                .onErrorResume(e -> Mono.error(new CustomException("导出失败")));
    }

    /**
     * 将响应体转换为Excel
     *
     * @param responseBody
     * @return
     */
    private Mono<byte[]> convertResponseBodyToExcel(ResponseBody<LoggingPO> responseBody) {
        return Mono.fromCallable(() -> {
            List<LoggingPO> dataList = responseBody.hits().hits().stream().map(Hit::source).filter(Objects::nonNull).toList();
            try (ByteArrayOutputStream out = new ByteArrayOutputStream(); Workbook workbook = new SXSSFWorkbook(200)) {
                //设置表格sheet
                Sheet sheet = workbook.createSheet("操作日志");

                //创建表头
                Row headerRow = sheet.createRow(0);
                for (int i = 0; i < EXCEL_HEADERS.length; i++) {
                    Cell cell = headerRow.createCell(i);
                    cell.setCellStyle(getOrCreateHeaderStyle(workbook, "logging-excel"));
                    cell.setCellValue(EXCEL_HEADERS[i]);
                }

                //填充数据
                SHEET_FILL.accept(dataList, sheet);

                workbook.write(out);
                return out.toByteArray();
            } catch (Exception e) {
                log.error("Excel生成失败", e);
                throw new CustomException("Excel生成失败");
            }
        }).subscribeOn(Schedulers.boundedElastic()); // 阻塞操作需要指定调度器
    }

    /**
     * 获取或创建表头样式
     *
     * @param workbook
     * @param styleKey
     * @return
     */
    @SuppressWarnings("all")
    private CellStyle getOrCreateHeaderStyle(Workbook workbook, String styleKey) {
        return STYLE_CACHE.computeIfAbsent(styleKey, key -> {
            CellStyle headerStyle = workbook.createCellStyle();
            Font headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerFont.setFontHeightInPoints((short) 12);
            headerStyle.setFont(headerFont);
            headerStyle.setAlignment(HorizontalAlignment.CENTER);
            return headerStyle;
        });
    }

    /**
     * 填充表格数据
     */
    private static final BiConsumer<List<LoggingPO>, Sheet> SHEET_FILL = (dataList, sheet) -> {
        for (int i = 0; i < dataList.size(); i++) {
            LoggingPO item = dataList.get(i);
            Row row = sheet.createRow(i + 1);
            row.createCell(0).setCellValue(item.getIp());
            row.createCell(1).setCellValue(item.getUri());
            row.createCell(2).setCellValue(item.getContent());
            row.createCell(3).setCellValue(item.getMethod());
            row.createCell(4).setCellValue(item.getParam());
            row.createCell(5).setCellValue(item.getBody());
            row.createCell(6).setCellValue(item.getDuration());
            row.createCell(7).setCellValue(item.getCode());
            //处理时间，把时间戳转换为日期格式
            String time = Instant.ofEpochMilli(item.getCreateTime()).atZone(ZoneId.systemDefault()).format(ApiConst.DATE_FORMATTER_4);
            row.createCell(8).setCellValue(time);
        }
    };
}
